#!/usr/bin/env python

#   Trolls Outta Luckland v1.1.0
#   Copyright 2009-2010, Gummbum
#
#   This file is part of Trolls Outta Luckland.
#
#   Trolls Outta Luckland is free software: you can redistribute it
#   and/or modify it under the terms of the GNU General Public License
#   as published by the Free Software Foundation, either version 3 of
#   the License, or (at your option) any later version.
#
#   Trolls Outta Luckland is distributed in the hope that it will be
#   useful, but WITHOUT ANY WARRANTY; without even the implied warranty
#   of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with Trolls Outta Luckland.  If not, see
#   <http://www.gnu.org/licenses/>.

"""
To do:
    v1.1.0:
    *   py2exe:
        *   [done] Figure it out. Make a Windows distro.
        *   [done] Pick a few songs from the bunch for the Windows package.
        *   Update docs w/ URLs to source.
        *   [done] Remove tips from docs. Hide 'em somewhere safe. People
            should be allowed the reward of discovering secrets. :)
Done:
    v1.1.0:
    *   Fonts got a facelift.
    *   Fps class now uses font name.
    *   Changed Destroyer class to drop a load of slowly moving mines.
        Number of mines increase with game difficulty.
    *   Changed scoring for missiles to put them more in line with mines:
        shooting yields low score to discourage commendation farming;
        on-screen when completing level gives 5X the PATROL score to
        encourage some commendation boosting strategies; death gives nothing.
    *   Made enemy laser fire a little less aggressive. With lasers,
        missiles, mines, and ramming craft there is a lot going on
        during the peaks.
    *   Added F12, RMB, LMB, and Visible Pointer to HUD.
    *   High scores now have commas.
    *   Changes for Pygame 1.8.1 compatibility:
        *   In enemy.py, powerups.py, ui.py, utils.py: replaced rect.copy()
            with pygame.Rect(rect).
        *   In gumm_sound.py: replaced mixer.pre_init(buffer=1024) with
            mixer.pre_init(0,0,0,1024).
    *   Narrowed song selection.
    *   Added ui.TextReader class for in-game Help and Credits.
    *   Fixed: Sky no longer layers planets behind galaxies.
    *   Fixed: utils.load_image(): using surface.copy() on a cached image
        no longer produces a copy with alpha=0. It now preserves the
        original alpha. (Bug in Surface.copy()?)
    v1.0.0:
    *   GUMMBUM high score: Wave 29, Commendations 357,467! Beat dat. :)
    *   Added missile attack to Kamikaze class enemies. A royal PITA after
        wave 10. I love em. :)
    *   Added aft laser cannon to compensate for missile pursuit.
    *   Reduced laser aggression on all enemy classes.
    *   Tweaked enemy fleets a tiny bit. Some are now harder, some easier.
    *   Now using pygame.mixer.music to play songs.
    *   Replaced mobility code in LaserBlast classes with Trig borrowed from
        PyMike's PYGL2D.
    *   Added more sky objects for scenic variety.
    *   Fixed: enemy sprites now go into the correct layer of the master
        sprite group.
    *   Removed obsolete SpriteStripAnim's cache hit/miss counters,
        cPickle and bz2 imports, and X_dump_hitmasks method.
    *   Removed TWITCHY turn method from player ship.
    *   Tidied up Now Playing: .wav|.ogg stripped from song name.
    v0.0.7:
    *   GUMMBUM high score: Wave 19, Commendations 207,160! Don't feel bad, I
        choked before wave 10 many times. Only survived wave 15 twice. :)
    *   Added psyco support.
    *   Added a debug hud. Moved debug items out of the game hud.
    *   Added floating score and bonus text.
    *   Added fourth row to standby formation for special elites.
    *   Added enemy Kamikaze. Required many adjustments to accommodate the
        more complex behavior.
    *   Added enemy Destroyer with barrage attacks.
    *   Added a "Loading enemy ship i/n..." message since hitmasks were
        taking a while to generate.
    *   Added dual-path convoy handling to Factory, and a number of such
        convoys.
    *   Shortened redundant enemy.Enemy* class names.
    *   Music shuffler is no longer completely random. The list is now split
        in half, and the halves shuffled to avoid replaying songs that just
        played.
    *   Fancier (harder to ace) convoy patterns. Who wants to ace these,
        anyway? The points suck.
    *   Tweak: Factory now uses a more gradual approach to increase number of
        baddies on patrol. Old way was instantly extremely hard at level 10.
    *   Highscore and settings files are now saved to ~/.trollsouttaluckland/.
    *   Generating hitmasks at runtime still takes a very long time, up to
        20 seconds on a slower computer. Reverted to generating Python code
        for hitmask modules, this time breaking them up into smaller modules.
        If the system has 300 MB free they will compile as part of initial
        game load. Otherwise, setup.py can compile them on low-mem systems.
    *   Fixed: Debug L was active in menu or marquis with interesting results.
    *   Fixed: Jitter in image used by Ship on sprite sheet SPACE1x150.png.
        One of the images was reversed on sprite sheet.
    *   Fixed: Jitter in image used by Sweeper on sprite sheet SPACE2x150.png.
        One of the images was reversed on sprite sheet.
    *   Fixed: Player ship took weird turn at start of waves 2+.
    *   Fixed: F12 works in marquis.
    *   Fixed: Game._signon would not choose a saved name unless a name
        was typed, and then would choose the typed name anyway.
    *   Fixed: Game could not find lib and content directories when cwd
        differs from game installation directory. Install dir discovery is
        smarter now.
    v0.0.6:
    *   Better "Loading..." screen.
    *   Load an enemy of each kind in Game.run to generate the hitmasks.
    *   Got rid of huge border when in windowed display.
    *   Reduced height requirement so game will fit on 1024x768 monitors.
    *   Tweaked factory: implemented per-type max_on_patrol instead of the
        shared max_on_patrol. Whilst in there, the patrol dispatcher was
        enhanced to turn out more interesting combinations.
    *   Tweaked the algorithm that increases max-on-patrol over time.
        *   For each game, every five waves the default for all enemy types
        increases by 1. Net effect: every five waves the wave starts out a
        bit harder.
        *   For each wave, about every 30 seconds a random enemy type's max
            will be increased by 1. This is cumulative for the wave only. It
            is reset to the game default when the next wave starts. Net
            effect: every 30 seconds the wave gets a bit harder.
    *   Fixed: Menu and marquis sounds were playing over music. They no
        longer play if a song is playing.
    v0.0.5:
    *   Fixed "ImportError: Bad magic number in lib/game_globals.pyc".
    v0.0.4:
    *   Added rotating images to sprites.
    *   Changed player ship steering dynamics.
    *   Mouse homes to ship at level start; homes to saved position if
        gameplay is interrupted (escape key).
    *   Windowed and fullscreen are choices now. Set in game_globals.py. Use
        F12 to toggle.
    *   Game speed derives from measured FPS. Classes' internal speed
        compensates.
    *   Enlarged SPACE*.png and Explode*.png sprite strips. <3 GIMP!
    *   New look for laser shots.
    *   Baddies fire directly at player if facing player.
    *   Sprite strip loader creates arbitrary number of rotated images
        and hitmasks. Hitmask generation very slow! Caching optimization
        implemented. Still slow for the many images needed, but works.
    *   New sign-on UI uses keyboard instead of mouse. Remembers last
        player. Added player picker, which selects from high score holders.
    *   Fixed: Stage stat updated immediately after killing last baddy.
        It now updates when next the level's herald posts.
    *   Fixed: Enemies warp when returning to standby formation.
    *   F12 toggles fullscreen.
    *   Mood music for Game._play(). Keys for: change track, mute, music
        volume, FX volume, and master volume. Settings are saved.
    *   EnemyFactory._make_fleet: optimized generation of enemy paths. Level
        loading speeded up greatly.
    *   SpriteSheetAnim performance. The game requires a lot of sprites, and
        a lot of hitmasks. Hitmask generation is extremely expensive. Ship
        needs 3 images * 360 rotations = 1080. Each Enemy needs
        3 images * 90 = 270. Just two Enemy subclasses takes over 5 seconds
        to gen hitmasks, a big ugly hitch in gameplay. Did the following to
        minimize that:
        *   Pre-generate hitmasks as Python modules. Compile to .pyc. Ship
            takes 2 GB memory to compile, but it loads very fast.
        *   Ship construction moved up to program load.
    v0.0.3:
    *   run.py is snowballing. Moved some classes out to window.py,
        high_scores.py, player.py, and ui.py.
    *   Graphic FX added.
    *   Sound FX added.
    v0.0.2:
    *   Path points optimized to reduce calculate_bezier() times. Steps
        reduced from 300 to 200: much faster level generation.
    *   Image cache.
    *   Much UI.
    *   Enemies shoot back.
    *   Integrated game loop states with intuitive menu.
    *   Dashboard: ship counter, score, stage, player name.
    *   Player name input.
    *   High scores.
    v0.0.1:
    *   Window, ship, pointer, and swerving enemies to shoot.
"""

__version__ = '1.1.0'

## Python imports
import fnmatch
import imp
import os
from os.path import join as join_path, dirname, realpath, abspath
import sys
import random
import time

def get_main_dir():
    """needed for py2exe compatibility"""
    def main_is_frozen():
        return (hasattr(sys, "frozen") or # new py2exe
                hasattr(sys, "importers") # old py2exe
                or imp.is_frozen("__main__")) # tools/freeze
    if main_is_frozen():
        # Program run from an executable, e.g. py2exe
        return os.path.dirname(sys.executable)
    elif os.path.dirname(sys.argv[0]) != '':
        # Program run as: python somepath/prog.py
        return os.path.dirname(sys.argv[0])
    else:
        # Program run as: python prog.py
        return sys.path[0]

PROGRAM_DIR = get_main_dir()
os.chdir(PROGRAM_DIR)
sys.path.insert(0, os.path.join(PROGRAM_DIR,'lib'))

try:
    ## Pygame imports
    import pygame
    from pygame.locals import (
        Color,
        QUIT, KEYDOWN, KEYUP, MOUSEBUTTONDOWN, MOUSEBUTTONUP, USEREVENT,
        K_ESCAPE, K_BACKSPACE, K_RETURN, K_UP, K_DOWN, K_F12,
        K_MINUS, K_UNDERSCORE, K_PLUS, K_EQUALS,
        K_LEFTBRACKET, K_RIGHTBRACKET,
        K_PERIOD, K_COMMA, K_LESS, K_GREATER,
        K_0, K_1, K_2, K_3, K_4, K_5, K_6, K_7, K_8, K_9,
        K_d, K_g, K_h, K_i, K_k, K_l, K_m, K_p, K_q, K_s, K_t, K_v,
    )
    ## Game library imports
    import game_globals
    from game_globals import *
    import enemy
    from enemy import Factory, Enemy
    from high_scores import SaveScore, HighScores
    from fps import Fps
    import gumm_sound
    from pixelperfect import *
    from player import Ship
    from powerups import PartsShop, ExtraShipDebris
    import settings
    from sky import Sky
    from utils import (
        calc_angle, draw_text, get_font, load_image,
        mouse_to_game, game_to_mouse, split_thousands,
    )
    from ui import (
        color as ui_color,
        layout as ui_layout,
        UILabel, Herald, TextReader, PlayerName, ShipCounter,
        Score, NowPlaying, Stage, Stat, Statf, Pointer,
    )
    from window import Window
except ImportError, e:
    print 'ImportError: %s' % e
    print '    in file "%s"' % os.path.abspath(sys.argv[0])

game = None
jukebox = None

class MusicTracks(list):
    def __init__(self):
        self.iter()
    def iter(self):
        self.i = 0
        return self
    def next(self):
        if self.i >= len(self):
            self.i = 0
        item = self[self.i]
        self.i += 1
        return item
    def shuffle(self):
        n = len(self)
        t0 = self[n:-1]
        t1 = self[0:n]
        random.shuffle(t0)
        random.shuffle(t1)
        self[:] = t0 + t1
music_tracks = MusicTracks()

class SpriteGroups():
    def __init__(self):
        self.all = pygame.sprite.LayeredUpdates()
        self.players = pygame.sprite.Group()
        self.enemies = pygame.sprite.Group()
        self.player_shots = pygame.sprite.Group()
        self.enemy_shots = pygame.sprite.Group()
        self.enemy_bots = pygame.sprite.Group()
        self.powerups = pygame.sprite.Group()
        self.pointer = pygame.sprite.Group()
        self._groups = {
            0 : self.pointer,
            1 : self.player_shots,
            2 : self.enemy_shots,
            3 : self.enemy_bots,
            5 : self.players,
            7 : self.enemies,
            9 : self.powerups,
        }
        # Layers for self.all = LayeredUpdates()...
        # Obscured effects go in layer-1.
        # Obscuring effects go in layer+1.
        self.layers = {
            'pointer' : 0,
            'player_shots' : 1,
            'enemy_shots' : 2,
            'enemy_bots' : 3,
            'players' : 5,
            'enemies' : 7,
            'powerups' : 9,
        }
    def empty(self):
        for g in (self.player_shots, self.enemy_shots, self.enemy_bots,
                  self.enemies, self.powerups):
            for s in g.sprites():
                s.kill()
    def add(self, *sprites, **kwargs):
        """kwargs: layer='layer', over=False, under=False"""
        layer = kwargs.get('layer', None)
        over = kwargs.get('over', False)
        under = kwargs.get('under', False)
        l = self.layers[layer]
        if over is True:
            l += 1
        elif under is True:
            l -= 1
        self.all.add(layer=l, *sprites)
        if over or under:
            pass
        else:
            self._groups[l].add(*sprites)
    def collide_pve(self):
        a = self._groupcollide(self.enemy_bots, self.player_shots)
        b = self._groupcollide(self.players, self.enemies)
        c = self._groupcollide(self.enemies, self.player_shots)
        d = self._groupcollide(self.players, self.enemy_shots)
        e = self._groupcollide(self.players, self.enemy_bots)
        for hits in (a,b,c,d,e):
            for s in hits.keys():
                s.hit(hits[s])
    def collide_powerups(self):
        a = self._groupcollide(self.players, self.powerups)
        b = self._groupcollide(self.enemies, self.powerups)
        c = self._groupcollide(self.player_shots, self.powerups)
        d = self._groupcollide(self.enemy_shots, self.powerups)
        for hits in (a,b,c,d):
            for s in hits.keys():
                s.power_up(hits[s])
    def _groupcollide(self, groupa, groupb):
        hits = {}
        for s1 in groupa.sprites():
            for s2 in groupb.sprites():
                if check_collision(s1, s2):
                    if s1 not in hits:
                        hits[s1] = []
                    hits[s1].append(s2)
        return hits

class Game(object):
    def __init__(self, name=None):
        self._name = settings.name
        self.state = POST_MENU
        self.prev_state = NEW
        self.in_progress = False
        self.bench_fps = FPS / 0.9
        self.max_fps = FPS
        self.game_speed = 1 
        self.save_mouse = pygame.mouse.get_pos()
        #
        self.window = Window()
        self.groups = None
        self.menu = None
        self.fps = None
        self.pointer = None
        self.sky = None
        self.player = None
        self.enemy_factory = None
        self.stats = None
        self.hud = None
        self.debugs = None
        #
        self.name = None
        self.stage = None
        self.score = None
        self.ship_counter = None
        self.now_playing = None
        self.high_scores = None
        self.parts_shop = None
        #
        self.fx_volume = settings.fx_volume
        self.music_volume = settings.music_volume
        self.music_paused = False
        self.show_hud = True
        self.show_debugs = True
        self._invulnerable = False
        self._grab_mouse = True
        self.pause = False
    def run(self):
        self._init()
        while 1:
            self._handle_events()
            state = self.state
            if state == PLAY:
                self._play()
            elif state == GET_MENU:
                self._get_menu()
            elif state == POST_MENU:
                self._post_menu()
            elif state in (FIRST,NEW):
                self._new()
            elif state == END:
                self._end()
            self.fps.tick(self.max_fps)
            self.window.flip()
    def set_state(self, state):
        if state in (PLAY,END):
            self.prev_state = state
        self.state = state
    def adjust_fx_volume(self, adjust):
        self.fx_volume = jukebox.adjust_type_volume(adjust, SOUND_FX)
    def adjust_music_volume(self, adjust):
        self.music_volume = jukebox.adjust_type_volume(adjust, SOUND_MUSIC)
        master_volume = jukebox.get_master_volume()
        music_volume = jukebox.get_type_volume(SOUND_MUSIC)
        pygame.mixer.music.set_volume(master_volume*music_volume)
    def adjust_master_volume(self, adjust):
        jukebox.adjust_master_volume(adjust)
        master_volume = jukebox.get_master_volume()
        music_volume = jukebox.get_type_volume(SOUND_MUSIC)
        pygame.mixer.music.set_volume(master_volume*music_volume)
    def play_song(self, interrupt=False):
        if len(music_tracks) == 0:
            return
        if interrupt and self.now_playing.text != 'none':
            pygame.mixer.music.fadeout(1000)
        if not self.song_is_playing():
            name = music_tracks.next()
            self.now_playing.set_text(name)
            pygame.mixer.music.load(join_path('music', name))
            pygame.mixer.music.play()
            master_volume = jukebox.get_master_volume()
            music_volume = jukebox.get_type_volume(SOUND_MUSIC)
            pygame.mixer.music.set_volume(master_volume*music_volume)
            pygame.mixer.music.set_endevent(SONG_ENDED_EVENT)
    def song_is_playing(self):
        return pygame.mixer.music.get_busy()
    def play_fx(self, name, mutex=False):
        if mutex:
            sound = jukebox[name]
            sound.play_mutex()
        else:
            sound = jukebox[name]
            sound.play()
    def update_bg(self, groups=[], all=False, standby=None, stats=True):
        self.window.clear()
        self.sky.update()
        if all:
            self.groups.all.update()
        else:
            self.groups.pointer.update()
        if self.enemy_factory is not None and standby is not None:
                self.enemy_factory.update(standby_only=standby)
        for obj in groups:
            obj.update()
        if stats:
            self._update_hud()
            self._update_debugs()
            self.stats.update()
        self.menu.update()
    def draw_bg(self, groups=[], all=False, stats=True):
        surface = self.window.surface
        self.sky.draw(surface)
        if all:
            self.groups.all.draw(surface)
        else:
            self.groups.pointer.draw(surface)
        for obj in groups:
            obj.draw(surface)
        if stats:
            self.stats.draw(self.window.surface)
        if self.state == GET_MENU:
            self.menu.draw(surface)
    def speed(self):
        return self.game_speed
    def wave(self):
        return self.stage.value
    def quit(self):
        if self.high_scores is not None:
            self.high_scores.put(
                SaveScore(self.name.text, self.stage.value+1,
                    self.score.value))
        settings.save()
        self._text_reader(join_path('data','credits.txt'))
        pygame.display.quit()
        sys.exit()
    def _init(self):
        self.fps = Fps(
            self.window._surface, ui_layout().fps, fg=ui_color('system'),
            font_name='hud')
        self.bench_fps = self.fps.benchmark()
        if FPS == 0:
            if self.bench_fps > LIGHT_SPEED:
                if self.bench_fps > LIGHT_SPEED + LIGHT_SPEED*0.1:
                    self.max_fps = LIGHT_SPEED
                else:
                    self.max_fps = LIGHT_SPEED * 0.9
            else:
                self.max_fps = self.bench_fps * 0.9
        self.game_speed = LIGHT_SPEED / self.max_fps
        if DEBUG:
            print 'Benchmark: BenchFPS=%.2f; MaxFPS=%.2f GameSpeed=%.2f' % (
                self.bench_fps, self.max_fps, self.game_speed)
        self.groups = SpriteGroups()
        self.menu = pygame.sprite.Group()
        self.pointer = Pointer()
        self.groups.add(self.pointer, layer='pointer')
        #
        self.sky = Sky()
        self.update_bg(stats=False)
        self.draw_bg(stats=False)
        if os.access(join_path('lib', 'player0.pyc'), os.F_OK) or \
           os.access(join_path('lib', 'player0.pyo'), os.F_OK):
            fmt = 'Loading ships %d/%d...'
        else:
            fmt = 'Compiling ships for the first time %d/%d...'
        i = 1
        n = len(enemy._UNIT_TYPES) + 1
        h0 = Herald('Trolls Outta Luckland', -2, False, font_name='title')
        h2 = Herald('Copyright 2009-2010, Gummbum', 0)
        h3 = Herald('License Gnu GPL version 3', 1)
        h1 = Herald(fmt%(i,5), 3)
        [self.window.surface.blit(h.image, h.rect) for h in (h0,h1,h2,h3)]
        self.window.flip()
        self.player = Ship()
        self.high_scores = HighScores()
        # load and cache enemy ship images and hitmasks
        started = time.time()
        for UnitClass in enemy._UNIT_TYPES.values():
            i += 1
            h1 = Herald(fmt%(i,n), 3)
            self.update_bg(stats=False)
            self.draw_bg(stats=False)
            [self.window.surface.blit(h.image, h.rect) for h in (h0,h1,h2,h3)]
            self.window.flip()
            UnitClass((0,0),(0,0))
        # new game attributes, ship, and enemies
        self.stats = pygame.sprite.Group()
        self.score = Score()
        self.ship_counter = ShipCounter()
        self.stage = Stage()
        self.now_playing = NowPlaying()
        self.parts_shop = PartsShop()
        self.stats.add(self.fps, self.score, self.ship_counter, self.stage,
            self.now_playing)
        self._make_hud()
        self._make_debugs()
        #
        h1 = Herald('Loading sound...', 3)
        self.update_bg(stats=False)
        self.draw_bg(stats=False)
        [self.window.surface.blit(h.image, h.rect) for h in (h0,h1,h2,h3)]
        self.window.flip()
        jukebox.load_sound(join_path('sound','46485__PhreaKsAccount__comms3.ogg'),
            'computer greets', volume=0.6, sound_type=SOUND_FX)
        jukebox.load_ascii_file(join_path('sound','erhboom1.dat'),
            'sign on', volume=0.8, sound_type=SOUND_FX)
        jukebox.load_ascii_file(join_path('sound','dtdspatialharmony.dat'),
            'high scores', volume=0.8, sound_type=SOUND_FX)
        jukebox.load_ascii_file(join_path('sound','pencap.dat'),
            'click', volume=1, sound_type=SOUND_FX)
        jukebox.load_ascii_file(join_path('sound','scareself.dat'),
            'wait input', volume=0.7, sound_type=SOUND_FX)
        jukebox.load_sound(join_path('sound','power_up_16893.ogg'),
            'power up', volume=1, sound_type=SOUND_FX)
        jukebox['marquis'] = jukebox['sign on']
        jukebox.set_type_volume(self.fx_volume, SOUND_FX)
        jukebox.set_type_volume(self.music_volume, SOUND_MUSIC)
        while time.time() - started < 5:
            pygame.time.wait(500)
        #
#        self._text_reader(join_path('data','help.txt'))
        self._sign_on()
        if not CODER_PASS:
            self._post_high_scores()
        self.menu_click = None
    def _play(self):
        self.play_song()
        # do player vs. environment
        window = self.window
        player = self.player
        groups = self.groups
        self.window.clear()
        self.sky.update()
        self.sky.draw(self.window.surface)
        if not self._invulnerable:
            groups.collide_pve()
        groups.collide_powerups()
        if len(groups.players.sprites()) == 0:
            self.parts_shop.lose()
            self.set_state(END)
        if self.enemy_factory.is_done() and \
        len(groups.enemies.sprites()) == 0:
            self.set_state(END)
        groups.all.update(window.rect)
        player.weapon.fire(player.state)
        self.enemy_factory.update()
        self.stats.update()
        self._update_hud()
        self._update_debugs()
        groups.all.draw(window.surface)
        self.stats.draw(window.surface)
    def _new(self):
        self.menu_click = None
        self.high_scores.put(
            SaveScore(self.name.text, self.stage.value+1,
                self.score.value))
        if self.in_progress:
            self._post_high_scores()
        self.stage.reset()
        self.score.reset()
        self.ship_counter.reset()
        self.groups.empty()
        # do ready-set-go theatrics
        self._kill_heralds()
        rect = self.window.rect
        self.pointer.go_home()
        self.player.reset()
        self.groups.add(self.player, layer='players')
        self.enemy_factory = Factory()
        self.ship_counter.reset()
        self.parts_shop.reset()
        self.set_state(PLAY)
        self.ship_counter.lose()
        self._marquis()
        self.pointer.go_home()
        self.in_progress = True
    def _post_menu(self):
        if self.in_progress:
            self.menu.add(Herald('Resume Battle', 0, font_name='menu'))
        self.menu.add(Herald('New Battle', 1, font_name='menu'),
                      Herald('Space Heroes', 2, font_name='menu'),
                      Herald('Help', 3, font_name='menu'),
                      Herald('Credits', 4, font_name='menu'),
                      Herald('AWOL', 5, font_name='menu'))
        self.set_state(GET_MENU)
        pygame.event.get()
    def _get_menu(self):
        if not self.song_is_playing():
            self.play_fx('wait input', True)
        if self.menu_click is not None:
            for s in self.menu.sprites():
                if s.rect.collidepoint(mouse_to_game(self.menu_click)):
                    self.play_fx('click')
                    if s.text == 'Resume Battle':
                        self.menu.empty()
                        self._marquis()
                        if self.in_progress:
                            self.pointer.set_real_pos(self.save_mouse)
                        self.state = self.prev_state
                    elif s.text == 'New Battle':
                        self.menu.empty()
                        self.set_state(NEW)
                    elif s.text == 'Space Heroes':
                        jukebox['wait input'].fadeout()
                        self._post_high_scores()
                    elif s.text == 'Help':
                        jukebox['wait input'].fadeout()
                        self._text_reader(join_path('data','help.txt'))
                    elif s.text == 'Credits':
                        jukebox['wait input'].fadeout()
                        self._text_reader(join_path('data','credits.txt'))
                    elif s.text == 'AWOL':
                        self.quit()
        else:
            for s in self.menu.sprites():
                if s.rect.collidepoint(self.pointer.get_pos()):
                    if s.pulse is False:
                        s.set_pulse(True)
                elif s.pulse is True:
                    s.set_pulse(False)
        self.menu_click = None
        if self.in_progress:
            self.update_bg()
        else:
            self.update_bg(all=True, standby=True)
        self.draw_bg(all=True)
        if self.state != GET_MENU and not self.in_progress:
            jukebox['wait input'].fadeout()
    def _next_level(self):
        self.high_scores.put(
            SaveScore(self.name.text, self.stage.value+1, self.score.value))
        for s in self.groups.all.sprites():
            if type(s) not in (Pointer,Ship,ExtraShipDebris):
                s.kill()
        self.pointer.go_home()
        self.player.reset()
        self.stage.add()
        self.enemy_factory = Factory()
        self._marquis()
        self.pointer.go_home()
    def _end(self):
        factory = self.enemy_factory
        anims = False
        for s in self.groups.enemy_bots:
            s.hit()
        for s in self.groups.all.sprites():
            isanim = True
            if issubclass(type(s), Enemy):
                if s.angle in (0,360):
                    isanim = False
                else:
                    isanim = True
            elif issubclass(type(s), (Pointer,Ship,ExtraShipDebris)):
                isanim = False
            if isanim:
                anims = True
                break
        if anims or not factory.is_done():
            # pass until anims and/or factory finish
            self._handle_events()
            self.update_bg(all=True, standby=True)
            self.draw_bg(all=True)
        elif self.player.alive():
            # player vanquished the wave
            self._next_level()
            self.set_state(PLAY)
        elif self.ship_counter.lives <= 1:
            # wave vanquished player's last ship
            self.in_progress = False
            self.set_state(POST_MENU)
            self.high_scores.put(
                SaveScore(self.name.text, self.stage.value+1, self.score.value))
            self._post_high_scores()
        else:
            # wave vanquished player; player has more ships
            self.player.reset()
            self.ship_counter.lose()
            self.groups.add(self.player, layer='players')
            self.set_state(PLAY)
            if len(self.groups.enemies.sprites()) == 0:
                self._next_level()
            else:
                self.pointer.go_home()
                self._marquis()
    def _kill_heralds(self):
        for s in self.stats.sprites():
            if isinstance(s, Herald):
                s.kill()
    def _do_settings(self, key):
        if key == K_k:
            self.sky.reset(debug=True)
        elif key == K_m:
            if pygame.mixer.music.get_busy():
                if self.music_paused:
                    pygame.mixer.music.unpause()
                    self.music_paused = False
                else:
                    pygame.mixer.music.pause()
                    self.music_paused = True
        elif key == K_s:
            music_tracks.shuffle()
        elif key == K_t:
            self.play_song(interrupt=True)
        elif key == K_v:
            self.pointer.visible ^= True
        elif key == K_RIGHTBRACKET:
            self.adjust_fx_volume(0.05)
        elif key == K_LEFTBRACKET:
            self.adjust_fx_volume(-0.05)
        elif key == K_MINUS:
            self.adjust_music_volume(-0.05)
        elif key in (K_PLUS,K_EQUALS):
            self.adjust_music_volume(0.05)
        elif key in (K_COMMA,K_LESS):
            self.adjust_master_volume(-0.05)
        elif key in (K_PERIOD,K_GREATER):
            self.adjust_master_volume(0.05)
        elif key == K_F12:
            self.window.toggle_fullscreen()
        elif key == K_h:
            self._toggle_hud()
        elif DEBUG:
            if key == K_d:
                self._toggle_debugs()
            elif key == K_g:
                self._grab_mouse ^= True
            elif key == K_i:
                self._invulnerable ^= True
            elif key == K_l and self.state == PLAY:
                self._next_level()
            elif key == K_p:
                self.pause ^= True
            elif key == K_q:
                print_sprites()
            elif key in range(K_0,K_9) and self.enemy_factory is not None:
                self.enemy_factory._toggle_draw_path(key)
    def _handle_events(self):
        for e in pygame.event.get():
            if e.type == SONG_ENDED_EVENT:
                if self.in_progress:
                    self.play_song()
            elif e.type == KEYUP and e.key == K_F12:
                self.window.toggle_fullscreen()
            elif self.state == PLAY:
                if e.type == MOUSEBUTTONDOWN:
                    if self.player.state & (SHOOTING_FORE|SHOOTING_AFT):
                        pass
                    elif e.button == 1:
                        self.player.state |= SHOOTING_FORE
                    elif e.button == 3:
                        self.player.state |= SHOOTING_AFT
                elif e.type == MOUSEBUTTONUP:
                    if e.button == 1 and self.player.state & SHOOTING_FORE:
                        self.player.state &= ~SHOOTING_FORE
                        self.player.weapon.input_sync = 0
                        self.player.weapon.fire_rate = 0
                    elif e.button == 3 and self.player.state & SHOOTING_AFT:
                        self.player.state &= ~SHOOTING_AFT
                        self.player.weapon.input_sync = 0
                        self.player.weapon.fire_rate = 0
                elif e.type == KEYUP:
                    if e.key == K_ESCAPE:
                        if self.prev_state not in (POST_MENU,GET_MENU):
                            self.save_mouse = self.pointer.get_real_pos()
                            self.prev_state = self.state
                            self.set_state(POST_MENU)
                    else:
                        self._do_settings(e.key)
            elif self.state in (POST_MENU,GET_MENU):
                if e.type == MOUSEBUTTONDOWN:
                    self.menu_click = e.pos
                elif e.type == KEYUP:
                    self._do_settings(e.key)
            elif e.type == KEYUP:
                if e.key == K_ESCAPE:
                    if self.prev_state not in (POST_MENU,GET_MENU):
                        self.save_mouse = self.pointer.get_real_pos()
                        self.prev_state = self.state
                        self.set_state(POST_MENU)
                elif e.key == K_p:
                    self.pause ^= True
                else:
                    self._do_settings(e.key)
    def _wait_mouse_up(self):
        pygame.event.get()
        if pygame.mouse.get_pressed()[0] == True:
            while 1:
                if pygame.event.wait().type == MOUSEBUTTONUP:
                    break
        pygame.event.get()
    def _segue(self, duration, spacing, *messages):
        """Display one or more transition messages, a la "Please wait..."
        duration is the number of seconds to pause
        spacing spreads out the vertical spacing of messages (try 1 or 2)
        messages are the strings to display
        """
        group = pygame.sprite.Group()
        window = self.window
        for i in range(len(messages)):
            group.add(Herald(messages[i], i*spacing))
        elapsed = 0
        duration *= 1000
        while elapsed < duration:
            self.window.clear()
            self.sky.update()
            self.stats.update()
            group.update()
            self.sky.draw(window.surface)
            group.draw(window.surface)
            if self.in_progress:
                self.stats.update(window._surface)
            self.window.flip()
            self.fps.tick(self.max_fps)
            elapsed += self.fps.get_time()
            for e in pygame.event.get():
                if e.type == KEYDOWN:
                    self._do_settings(e.key)
    def _marquis(self):
        window = self.window
        pointer = self.pointer
        group = self.groups.all
        stats = self.stats
        stage = 'Attack Wave %d' % (self.stage.value+1)
        self.stats.add(Herald(stage, pulse=True))
        self._wait_mouse_up()
        save_mouse = self.pointer.get_real_pos()
        while 1:
            if not self.song_is_playing():
                self.play_fx('marquis', True)
            e = pygame.event.poll()
            if e.type == MOUSEBUTTONUP:
                self.play_fx('click')
                pygame.event.get()
                jukebox['marquis'].fadeout()
                self.pointer.set_real_pos(save_mouse)
                break
            elif e.type == KEYDOWN:
                self._do_settings(e.key)
            self.update_bg()
            self.draw_bg(groups=[group])
            window.flip()
            self.fps.tick(self.max_fps)
        self._kill_heralds()
    def _sign_on(self):
        jukebox['computer greets'].play()
        window = self.window
        surface = window.surface
        sky = self.sky
        group = pygame.sprite.Group()
        group.add(Herald('Trolls Outta Luckland', -2, False,
            font_name='title'))
        group.add(Herald('Combat systems locked.', 0, False))
        group.add(Herald('Voice ident required...', 1, False))
        h4 = Herald('1) [%s] Coffee and music, ship!'%self._name, 3, False)
        group.add(h4)
        h5 = Herald('2) Change of command, ship.', 4, False)
        h5.rect.left = h4.rect.left
        group.add(h5)
        # Same player or Change player name
        if CODER_PASS:
            have_name = True
        else:
            have_name = False
        pygame.event.clear()
        while not have_name:
            self.play_fx('sign on', True)
            e = pygame.event.poll()
            if e.type == KEYUP:
                if e.key == K_1:
                    self.play_fx('click')
                    have_name = True
                    break
                elif e.key == K_2:
                    self.play_fx('click')
                    break
            self.update_bg(groups=[group], stats=False)
            self.draw_bg(groups=[group], stats=False)
            window.flip()
            self.fps.tick(self.max_fps)
        group.empty()
        # player name workspace and display
        if have_name:
            player_name = self._name
            player_edit = Herald(player_name, 1, False, font_name='text')
            player_edit.set_color(ui_color('stats'))
        else:
            group.add(
                Herald('Key in pilot handle for biometric ident...', 0, False))
            player_name = PLAYER_NAME_DEFAULT
            player_edit = Herald(player_name, 3, False, font_name='text')
            player_edit.set_color(ui_color('stats'))
            players = [player_edit]
            playersi = 0
            for o in self.high_scores.values():
                if o.name.isupper():
                    players.append(Herald(o.name, 1, False, font_name='text'))
            players_display = players[playersi:playersi+2]
            for i in range(len(players_display)):
                players_display[i].set_line(i+3)
            group.add(*players_display)
        # get keys, update player name
        while not have_name:
            self.play_fx('sign on', True)
            e = pygame.event.poll()
            if e.type == KEYUP:
                self.play_fx('click')
                if e.key == K_DOWN:
                    # step down the player list
                    if playersi < len(players)-1:
                        if len(players_display) == 3:
                            h = players_display.pop(0)
                            h.kill()
                        players[playersi].set_color(ui_color('system'))
                        playersi += 1
                        players[playersi].set_color(ui_color('stats'))
                        try:
                            players_display.append(players[playersi+1])
                            group.add(players[playersi+1])
                        except IndexError:
                            pass
                        if playersi in (0,len(players_display)-1):
                            line = 3
                        else:
                            line = 2
                        for i in range(len(players_display)):
                            players_display[i].set_line(line+i)
                elif e.key == K_UP:
                    # step up the player list
                    if playersi > 0:
                        if len(players_display) == 3:
                            h = players_display.pop()
                            h.kill()
                        players[playersi].set_color(ui_color('system'))
                        playersi -= 1
                        players[playersi].set_color(ui_color('stats'))
                        if playersi > 0:
                            players_display.insert(0, players[playersi-1])
                            group.add(players[playersi-1])
                        if playersi == 0:
                            line = 3
                        else:
                            line = 2
                        for i in range(len(players_display)):
                            players_display[i].set_line(line+i)
                elif e.key > 255:
                    pass
                elif e.key == K_RETURN:
                    # process return
                    if playersi > 0:
                        player_name = players[playersi].text
                        have_name = True
                    elif player_name != PLAYER_NAME_DEFAULT:
                        player_name = player_name.rstrip(RUBOUT)
                        player_edit.set_text(player_name)
                        have_name = True
                elif players[playersi] is not player_edit:
                    pass
                elif e.key == K_BACKSPACE:
                    # process backspace
                    l = list(player_name)
                    i = player_name.find(RUBOUT)
                    if i > 0:
                        i -= 1
                    l[i] = RUBOUT
                    player_name = ''.join(l)
                    player_edit.text = player_name
                    player_edit.render_text()
                elif chr(e.key).isalnum():
                    # process letter
                    i = player_name.find(RUBOUT)
                    if i > -1:
                        l = list(player_name)
                        l[i] = chr(e.key).upper()
                        player_name = ''.join(l)
                        player_edit.set_text(player_name)
            self.update_bg(groups=[group], stats=False)
            self.draw_bg(groups=[group], stats=False)
            window.flip()
            self.fps.tick(self.max_fps)
        group.empty()
        self.name = PlayerName(player_name)
        self.stats.add(self.name)
        self._name = player_name
        jukebox['sign on'].fadeout()
        if not CODER_PASS:
            jukebox['power up'].play()
            self._segue(5, 2,
                'Identity confirmed, Commander %s.' % self._name,
                'Powering up combat systems.',
                'The ship is under your command.')
    def _post_high_scores(self):
        # This is not smart. Positions are hard-coded and depend on the
        # font size of the Herald class, and width of the Window class.
        surf = self.window.surface
        rect = self.window.rect
        pointer = self.pointer
        group = pygame.sprite.Group()
        line = -3
        group.add(Herald('Most Decorated Space Heroes', line,
            font_name='title'))
        compare = lambda a,b: cmp(b.score, a.score)
        scores = sorted(self.high_scores.values(), compare)
        line += 2
        name = Herald('Name', line, False, font_name='system')
        stage = Herald('Wave', line, False, font_name='system')
        score = Herald('Commendations', line, False, font_name='system')
        name.rect.left = rect.left + 150
        score.rect.right = rect.right - 150
        stage.rect.centerx = rect.centerx + 15
        group.add(name, stage, score)
        line += 1
        for o in scores:
            if self.name.text == o.name and \
            self.stage.value+1 == o.stage and \
            self.score.value == o.score:
                pulse = True
            else:
                pulse = False
            name = Herald(o.name, line, pulse, font_name='game')
            stage = Herald(repr(o.stage), line, pulse, font_name='game')
            score = Herald(split_thousands(repr(o.score)), line, pulse,
                font_name='game')
            name.rect.left = rect.left + 150
            score.rect.right = rect.right - 150
            stage.rect.right = rect.centerx + 25
            group.add(name, stage, score)
            line += 1
            if line > 7:
                break
        line += 2
        group.add(Herald('Acknowledge', line, True, font_name='menu'))
        self._wait_mouse_up()
        save_state = self.state
        self.set_state(HIGHSCORES)
        while 1:
            self.play_fx('high scores', True)
            e = pygame.event.poll()
            if e.type == MOUSEBUTTONUP:
                jukebox['high scores'].fadeout()
                self.play_fx('click')
                break
            self.update_bg(groups=[group], stats=False)
            self.draw_bg(groups=[group], stats=False)
            self.window.flip()
            self.fps.tick(self.max_fps)
        self.set_state(save_state)
    def _text_reader(self, filename):
        rect = pygame.Rect(self.window.rect)
        rect.left += 150
        rect.width -= 300
        self._wait_mouse_up()
        save_state = self.state
        self.set_state(HELP)
        f = open(filename)
        help = TextReader(f.readlines(), rect=rect, font_name='text')
        f.close()
        up_pressed = down_pressed = False
        mouse_wheeled = 0
        while 1:
            self.play_fx('high scores', True)
            e = pygame.event.poll()
            if e.type == KEYDOWN:
                if e.key == K_UP:
                    up_pressed = True
                elif e.key == K_DOWN:
                    down_pressed = True
            elif e.type == KEYUP:
                if e.key == K_UP:
                    up_pressed = False
                elif e.key == K_DOWN:
                    down_pressed = False
                elif e.key == K_ESCAPE:
                    break
            elif e.type == MOUSEBUTTONUP:
                if e.button == 4:
                    mouse_wheeled += 1
                elif e.button == 5:
                    mouse_wheeled -= 1
                else:
                    break
            if down_pressed:
                help.scroll(-1)
            if up_pressed:
                help.scroll(1)
            if mouse_wheeled:
                mouse_wheeled = help.scroll(mouse_wheeled)
            self.update_bg(groups=[help], stats=False)
            self.draw_bg(groups=[help], stats=False)
            self.window.flip()
            self.fps.tick(self.max_fps)
        jukebox['high scores'].fadeout()
        self.play_fx('click')
        self.set_state(save_state)
    def _make_hud(self):
        # Should probably turn this into a class.
        fontw,fonth = get_font('hud').size('W')
        ## To add more to the hud...
        ## 1. Set this to the number of entries in dictionary self.hud.
        nr_stats = 11
        y = self.window.rect.bottom - (nr_stats+1)*fonth
        x = self.window.rect.right - 150
        ## 2. Add the entries(s) to the dict. Use Stat for static text,
        ##    Statf for a formatted text. If Stat, you are done.
        ## 3a. Add a statement to Game._update_hud (see step 3b).
        self.hud = {
            'MV' : Statf('Master volume (<>): %.2f', (x,y+fonth*0),
                        jukebox.get_master_volume()),
            'mv' : Statf('Music volume (-+): %.2f', (x,y+fonth*1),
                        self.music_volume),
            'fv' : Statf('Fx volume ([]): %.2f', (x,y+fonth*2),
                        self.fx_volume),
            'mp' : Statf('Music paused (M): %s', (x,y+fonth*3),
                        self.music_paused),
            'st' : Stat('Skip song track (T)', (x,y+fonth*4)),
            'sh' : Stat('Shuffle music (S)', (x,y+fonth*5)),
            'screen' : Stat('Fullscreen/Window (F12)', (x,y+fonth*6)),
            'point' : Statf('Visible mouse (V): %s', (x,y+fonth*7),
                        self.pointer.visible),
            'fore' : Stat('Front laser (LMB)', (x,y+fonth*8)),
            'aft' : Stat('Rear laser (RMB)', (x,y+fonth*9)),
            'hud' : Stat('Toggle game HUD (H)', (x,y+fonth*10)),
        }
        self.stats.add(self.hud.values())
        self._update_hud()
    def _toggle_hud(self):
        if self.show_hud:
            self.show_hud = False
            for s in self.hud.values():
                if s is not self.hud['hud']:
                    s.kill()
        else:
            self.show_hud = True
            for s in self.hud.values():
                if s is not self.hud['hud']:
                    self.stats.add(s)
    def _update_hud(self):
        if self.show_hud:
            hud = self.hud
            ## 3b. Add statements to update the values in dictionary Game.hud.
            ## 4. If user setting, add event code in Game._do_settings.
            hud['MV'].set_value(jukebox.get_master_volume())
            hud['mv'].set_value(self.music_volume)
            hud['fv'].set_value(self.fx_volume)
            hud['mp'].set_value(self.music_paused)
            hud['point'].set_value(self.pointer.visible)
    def _make_debugs(self):
        if not DEBUG:
            return
        # Should probably turn this into a class.
        fontw,fonth = get_font('hud').size('W')
        y = self.window.rect.top + 9
        x = self.window.rect.right - 150
        self.debugs = {
            'debugs' : Stat('Toggle debug HUD (D)', (x,y+fonth*0)),
            'fps' : Stat('BenchFPS: %d MaxFPS: %d' % (
                self.bench_fps, self.max_fps), (x,y+fonth*1)),
            'speed' : Stat('GameSpeed: %.1f' % self.speed(), (x,y+fonth*2)),
            'gm' : Statf('Grab mouse (G): %s', (x,y+fonth*3),
                        self._grab_mouse),
            'inv' : Statf('Player Invulnerable (I): %s', (x,y+fonth*4),
                        self._invulnerable),
            'sky' : Stat('New sky (K)', (x,y+fonth*5)),
            'nextlv' : Stat('Next wave (L)', (x,y+fonth*6)),
            'pause' : Stat('Pause (P)', (x,y+fonth*7)),
            'pointer' : Statf('Pointer visible (V): %s', (x,y+fonth*8),
                            self.pointer.visible),
            'path' : Stat('Show path (0-9)', (x,y+fonth*9)),
#            'sa' : Statf('Ship angle %d', (x,y+fonth*10),
#                        self.player.angle),
#            'mx' : Statf('Mouse pos (%d,%d)', (x,y+fonth*10),
#                        pygame.mouse.get_pos()),
#            'px' : Statf('Pointer pos (%d,%d)', (x,y+fonth*11),
#                        self.pointer.rect.center),
#            'fm' : Statf('Fighter max patrol: %.1f', (x,y+fonth*10), 0.0),
#            'sm' : Statf('Sweeper max patrol: %s', (x,y+fonth*11), 0.0),
#            'km' : Statf('Kamikaze max patrol: %s', (x,y+fonth*12), 0.0),
        }
        self.stats.add(self.debugs.values())
        self._update_debugs()
    def _toggle_debugs(self):
        if not DEBUG:
            return
        if self.show_debugs:
            self.show_debugs = False
            for s in self.debugs.values():
                if s is not self.debugs['debugs']:
                    s.kill()
        else:
            self.show_debugs = True
            for s in self.debugs.values():
                if s is not self.debugs['debugs']:
                    self.stats.add(s)
    def _update_debugs(self):
        if not DEBUG:
            return
        if self.show_debugs:
            debugs = self.debugs
            debugs['inv'].set_value(self._invulnerable)
            debugs['pointer'].set_value(self.pointer.visible)
            debugs['gm'].set_value(self._grab_mouse)
#            if self.enemy_factory is not None:
#                hud['fm'].set_value(
#                    self.enemy_factory.max_on_patrol[enemy.Fighter])
#                hud['sm'].set_value(
#                    self.enemy_factory.max_on_patrol[enemy.Sweeper])
#                hud['km'].set_value(
#                    self.enemy_factory.max_on_patrol[enemy.Kamikaze])
#            hud['sa'].set_value(self.player.angle)
#            hud['mx'].set_value(pygame.mouse.get_pos())
#            hud['px'].set_value(self.pointer.rect.center)
        if self.enemy_factory is not None:
            self.enemy_factory.draw_path()
        if self.pause:
            pygame.event.get()
            pygame.event.wait()
            self.pause = False

def main():
    global game, jukebox, music_tracks
    pygame.init()
    os.umask(0022)
    # Init the user's application directory for saves and settings.
    if not os.path.isdir(USER_DIR):
        os.makedirs(USER_DIR)
    # Make the system-wide jukebox
    jukebox = gumm_sound.jukebox
    jukebox.set_master_volume(settings.master_volume)
    for pat in ('*.ogg', '*.wav'):
        music_tracks.extend(fnmatch.filter(os.listdir('music'), pat))
    music_tracks.shuffle()
    # Make the game
    game_globals.game = game = Game()
    if False:
        import cProfile
#        cProfile.run('game.run()', 'run.prof')
        cProfile.run('game.run()')
    else:
        game.run()

if __name__ == '__main__':
    if USE_PSYCO:
        try:
            import psyco
            psyco.full()
            print 'psyco succeeded'
        except ImportError,e:
            print 'psyco failed:',e
    main()
